package seminar;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;

/**
 * A generation containing a list of children
 * @author Johannes Degler
 *
 */
public class Generation {

	/**
	 * Thread for creating new children
	 * @author Johannes Degler
	 *
	 */
	private class ChildCreatorThread extends Thread{

		private final Generation parents;

		public ChildCreatorThread(final Generation parents){
			this.parents = parents;
		}

		@Override
		public void run() {
			while(Generation.this.children.size() < Generation.GENERATION_SIZE){
				final Child parent1 = this.parents.getRandomChild(null);
				final Child parent2 =  this.parents.getRandomChild(parent1);
				this.createNewChild(parent1, parent2);
			}
		}

		/**
		 * Create a new child based on two parents
		 * @param parent1 First Parent
		 * @param parent2 Second Parent
		 */
		private void createNewChild(final Child parent1, final Child parent2){
			try {
				final Child child = new Child(parent1, parent2);
				if(Generation.this.children.size() < Generation.GENERATION_SIZE) //Creating a child takes some time, so better check again before adding
					Generation.this.children.add(child);
			} catch (final DistributionException e) {
				Generation.log.error("Caught distribution exception trying to create new child: " + e.getMessage(), e.getCause());
				e.printStackTrace();
			}
		}

	}

	/**
	 * Thread for evaluating the generation's children
	 * @author Johannes Degler
	 *
	 */
	private class EvaluationThread extends Thread{

		private final Set<Child> children = new HashSet<Child>();

		public void addChild(final Child child){
			this.children.add(child);
		}

		@Override
		public void run() {
			for(final Child child : this.children)
				child.evaluate();
		}
	}

	private static final double PARENT_MODIFIER = 0.025; //Lower values == more likely for "bad" children to be selected as a parent

	private static final Logger log = Logger.getLogger(Generation.class);

	private static final int GENERATION_SIZE = 2000;

	private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();

	static{
		Generation.log.info(Generation.CPU_COUNT + " available CPU cores found");
	}

	private final List<Child> children = Collections.synchronizedList(new ArrayList<Child>(Generation.GENERATION_SIZE));

	private final int number;

	private final boolean initialParent;

	/**
	 * New generation based on a parent generation
	 * @param parents Parent Generation
	 * @param number Number of the generation
	 * @throws DistributionException Thrown if distributing lectures fails (Shouldn't happen at this point)
	 */
	public Generation(final Generation parents,  final int number) throws DistributionException{
		this.number = number;
		Generation.log.debug("New generation: " + this);

		this.initialParent = false;

		//Sort children
		Collections.sort(parents.children);

		final Set<ChildCreatorThread> threads = new HashSet<ChildCreatorThread>();

		//Create threads
		for(int i = 0; i < Generation.CPU_COUNT; i++){
			final ChildCreatorThread thread = new ChildCreatorThread(parents);
			threads.add(thread);
			thread.start();
		}

		//Synchronize
		for(final ChildCreatorThread thread : threads)
			try {
				thread.join();
			} catch (final InterruptedException e) {
				Generation.log.error("Error joining threads: " + e.getMessage() + ". Exiting");
				e.printStackTrace();
				System.exit(-1);
			}
	}

	/**
	 * New initial generation
	 * @param number
	 */
	public Generation(final int number){
		this.number = number;
		Generation.log.debug("New initial generation: " + this);

		this.initialParent = true;

		for(int i = 0; i < Generation.GENERATION_SIZE; i++)
			this.children.add(new Child());
	}

	/**
	 * Evaluate the children of this generation
	 */
	public void evaluate(){
		Generation.log.debug("Evaluating generation " + this);

		//Create threads
		final List<EvaluationThread> threads = new ArrayList<EvaluationThread>(Generation.CPU_COUNT);
		for(int i = 0; i < Generation.CPU_COUNT; i++)
			threads.add(new EvaluationThread());
		for(int i = 0; i < this.children.size(); i++)
			threads.get(i%Generation.CPU_COUNT).addChild(this.children.get(i));

		//Start threads
		for(final EvaluationThread thread : threads)
			thread.start();

		//Synchronize
		for(final EvaluationThread thread : threads)
			try {
				thread.join();
			} catch (final InterruptedException e) {
				Generation.log.error("Error joining threads: " + e.getMessage() + ". Exiting");
				e.printStackTrace();
				System.exit(-1);
			}
	}

	/**
	 * Get the best child of this generation
	 * @return Best child
	 */
	public Child getBestChild(){
		Generation.log.debug("Searching for best child of generation " + this);
		int curMax = Integer.MIN_VALUE;
		Child curChild = null;

		for(final Child child : this.children)
			if(curChild == null || child.getPoints() > curMax){
				curMax = child.getPoints();
				curChild = child;
			}

		return curChild;
	}

	public List<Child> getChildren() {
		return this.children;
	}

	public double getAveragePoints(){
		long points = 0;
		for(final Child child : this.children)
			points += child.getPoints();
		return points/(double)children.size();
	}
	
	/**
	 * Select a random child
	 * @param exclude Child to exclude. Use null if no child should be excluded
	 * @return Random child
	 */
	private Child getRandomChild(final Child exclude){
		if(exclude == null)
			Generation.log.debug("Getting random child from generation " + this + ". No excluded child");
		else
			Generation.log.debug("Getting random child from generation " + this + ". Excluding child " + exclude);

		while(true)
			for(final Child child : this.children)
				if((exclude == null || exclude != child) && Math.random() < Generation.PARENT_MODIFIER)
					return child;
	}

	/**
	 * Distribute children's lectures. Should only be called for initial parent generation
	 * @throws DistributionException
	 */
	public void initialDistribute() throws DistributionException{
		if(!this.initialParent)
			throw new IllegalStateException("Initial distribution called for non-initial generation");

		for(final Child child : this.children)
			child.initialDistribute();
	}

	@Override
	public String toString() {
		return "Generation " + this.number;
	}
}
